全線預備~~ 開始射擊
StrictMode
延續昨天的內容把StrictMode認識完,做一顆按鈕觀察StrictMode,之後進入新的知識點,認識事件冒泡。
StrictMode 會在開發模式對包住的元件啟用額外檢查與警告,讓一些平常不容易重現的問題提早出現,而這些額外行為只在開發模式啟用,不會帶到正式建置。
| 項目 | React 行為 | 檢查對象 |
|---|---|---|
| 渲染的純粹性 | 額外呼叫元件本體等應保持純粹的函式 | 渲染時修改原始資料、執行外部動作,導致重複執行後結果改變 |
| Effect 的清理 | 在應用程式根部啟用時,初次掛載額外執行一輪設定與清理,再設定 | 忘記取消訂閱、清除計時器或關閉連線,造成資源重複建立 |
| ref 回呼的清理 | 額外執行回呼的設定與清理流程 | 沒有移除舊節點參照,造成參照資料持續累積 |
| 已棄用 API | 對部分舊 API 用法提出警告 | 程式仍依賴應該更新的舊寫法 |
例如一個 Effect 負責訂閱事件,正確流程應該能經過「訂閱 → 取消訂閱 → 再訂閱」,最後只保留一份訂閱。如果漏掉取消訂閱,這輪額外執行就可能讓同一個事件被處理兩次,提醒我們補上清理。
這就是 StrictMode 的實際用途:提早暴露重複執行、重新掛載時可能遇到的問題。
關於 Effect 與 ref 的寫法就留到後面吧。
day-1-hello-react 目前安裝的 React Router,預設瀏覽器入口已經用 StrictMode 包住整個應用程式,可以直接沿用。
React Router 框架提供的預設入口檔 entry.client.tsx 的用途與範例可參考這裡 React Router:entry.client.tsx。
如果使用的是自己調整過、尚未啟用 StrictMode 的入口,可以先只檢查首頁這一部分。在 app/routes/home.tsx 最上方新增:
import { StrictMode } from "react";
然後保留既有的 Route、Welcome 匯入與 meta 函式,只把 Home 改成:
export default function Home() {
return (
<StrictMode> {/* 入口尚未啟用 StrictMode 時,才需要在這裡包裝 */}
<Welcome />
</StrictMode>
);
}
這個局部寫法檢查 Welcome 與它底下的元件;沿用本專案預設入口時,Home 保持原本的 return <Welcome />; 即可,不必重複包。
但框架已經有了,就不用特地做了~
在 app/welcome/welcome.tsx 的 Welcome 函式外面新增:
export function StrictModeProbe() {
console.log("[觀察:渲染] StrictModeProbe 計算畫面");
function handleClick() {
console.log("[觀察:點擊] 按鈕收到一次操作");
}
return (
<section>
<h1>StrictMode 觀察練習</h1>
<p>畫面上只放一個觀察元件。</p>
<button type="button" onClick={handleClick}>
測試點擊
</button>
</section>
);
}
第一個 Log 放在元件本體,只要 React 呼叫這個元件就會執行。第二個 Log 放在事件處理函式裡,要操作按鈕才會執行。兩個前綴用來區分訊息來源。
原本匯入 Welcome 的那行暫時換成:
import { StrictModeProbe } from "../welcome/welcome";
接著我們替換首頁元件 <Welcome>,改成 <StrictModeProbe>,只修改 Home():
export default function Home() {
{/* 若前面採用局部 StrictMode,只要把 <Welcome /> 換成 <StrictModeProbe />,外層 <StrictMode> 保留 */}
return <StrictModeProbe />;
}
這一階段讓 Home 只回傳 <StrictModeProbe />。等頁面載入完成後,清空瀏覽器的 Console,關閉保留舊訊息(Preserve log),手動重新載入一次。
會看到元件渲染邏輯被額外執行,但事件處理函式不會被 StrictMode 額外呼叫。
「兩筆 Log」是這個簡化的初次載入觀察,不是所有元件永遠只會呼叫兩次的保證,也不能單靠 Log 次數推斷其他專案有沒有啟用 StrictMode。
這個正確的情況下,額外呼叫元件仍會得到相同的畫面,所以畫面不會多出第二份。StrictMode 提供的是額外執行的機會,讓我們提早觀察程式能否保持正確。 它不會替我們判斷所有業務結果,也不會自動比較兩次 Log 後宣告程式有錯。
如果把使用者操作後才應執行的資料寫入放在元件本體,額外渲染就可能重複執行,這能提醒操作造成的動作要放回事件處理函式。
Console 的訊息也要會看:
[觀察:渲染] 和 [觀察:點擊] 都是我們寫的 Log。hydration的相關內容:React:hydrateRoot
判斷的關鍵是「這段程式在什麼時候被執行」。 渲染是 React 呼叫元件、計算要呈現的畫面;事件處理則是在點擊等操作發生後,執行對應的處理函式(event handler)。
以剛才的範例來說明,元件本體的 Log 會在渲染時印出,handleClick 裡的 Log 則在操作按鈕時才印出。
| 程式片段 | 執行時機 | 執行內容 |
|---|---|---|
console.log("[觀察:渲染]...") |
React 呼叫 StrictModeProbe 進行渲染時 |
暫時記錄渲染,方便觀察 |
return (...) 裡的 JSX |
渲染時 | 產生畫面描述,並把 handleClick 交給 onClick |
handleClick 裡的 console.log("[觀察:點擊] …") |
使用者操作按鈕,React 呼叫 handleClick 時 |
回應這次點擊 |
接下來回到學習卡片:卡片上的筆記文字會隨著渲染呈現在畫面上;按下新增的按鈕後,事件處理函式才會把那張卡片的筆記印到 Console。
元件函式裡的計算,會隨著渲染執行,事件處理函式(event handler)則交給 React,在對應操作發生時呼叫。
例如,把函式傳給 <button> 的 onClick,就能回應按鈕操作。
先看一個小範例:
function MyButton() {
function handleClick() {
console.log("按鈕收到操作了。");
}
return (
<button type="button" onClick={handleClick}>
按鈕來了
</button>
);
}
handleClick 是我們取的函式名稱,習慣上處理事件會用 handle 開頭。接著處理的動作onClick 則是 React 按鈕的事件 prop,要注意大小寫喔。
上面定義的 handleClick 不會立刻執行它。onClick={handleClick} 是把函式交出去,handleClick() 才是呼叫函式。
意思就是,把 handleClick 這件事情,給 React 處理,當按鈕按下才會觸發事件。
以下幾個組合分別會這樣執行:
| 寫法 | 執行方式 |
|---|---|
onClick={handleClick} |
把函式傳給 React,操作按鈕時才呼叫 |
onClick={() => handleClick()} |
傳入箭頭函式,操作時由它呼叫 handleClick() |
onClick={handleClick()} |
渲染到這裡就先呼叫,並把回傳值當成事件處理函式;此例回傳 void,型別也不符合 |
onClick={() => handleClick} |
操作時只回傳函式本身,沒有呼叫,所以看不到訊息 |
到 app/routes/home.tsx,把 StrictModeProbe 的匯入改回 Welcome,並把元件換回 <Welcome />,我們要把上面講到的,實際應用到 Welcome()內。
NoteButton。在 Welcome() 函式外面新增:
type NoteButtonProps = {
title: string;
onRead: () => void;
};
function NoteButton({ title, onRead }: NoteButtonProps) {
return (
<button type="button" onClick={onRead}>
查看「{title}」筆記
</button>
);
}
onRead 是自訂元件的 prop 名稱,可以按照用途命名;NoteButton 裡真正的 HTML 按鈕,仍要接到 onClick 才會回應操作。
接著 Welcome() 函式裡,return 之前,新增處理筆記的函式。LearningPlan 型別沿用昨天的定義。
function handleReadNote(plan: LearningPlan) {
console.log(`[筆記] ${plan.title}:${plan.note}`);
}
找到 visiblePlans.map((plan) => ...) 裡的卡片,在原本顯示 {plan.note} 的段落後面加入 NoteButton。
把按鈕在 <LearningCard> 與 </LearningCard> 之間,作為卡片的 children:
<p className="text-gray-600">{plan.note}</p>
<NoteButton
title={plan.title}
onRead={() => handleReadNote(plan)}
/>
這裡需要把 plan 傳給 handleReadNote,所以用箭頭函式包住呼叫。渲染時先準備函式,等操作按鈕時,才拿對應的計畫執行。直接寫 onRead={handleReadNote(plan)} 就會提早執行。
onRead: () => void 描述的是一個不需要參數、沒有要使用其回傳值的函式,實際執行時,外層的箭頭函式會替它呼叫 handleReadNote(plan)。
沿著程式的呼叫路徑,可以整理成以下:
操作 NoteButton 裡的按鈕
-> React 呼叫 onClick 收到的 onRead
-> 執行 Welcome 傳入的 () => handleReadNote(plan)
-> Console 印出那張卡片的標題與筆記
在 Welcome 外面新增 EventDemo。這裡省略樣式,只看內層按鈕和外層區塊各自的事件:
function EventDemo() {
return (
<section>
<h2>事件傳遞練習區</h2>
<div onClick={() => console.log("[事件] 外層區塊收到點擊")}>
<p>這裡是外層區塊。</p>
<button
type="button"
onClick={() => console.log("[事件] 內層按鈕收到點擊")}
>
測試事件傳遞
</button>
</div>
</section>
);
}
再到 Welcome 回傳的 JSX,放在最前面,方便操作:
<h1 className="text-2xl font-bold">我的 React 學習卡片</h1>
<EventDemo />
當我們在測試事件傳遞按一下,會一次觸發兩個區塊的事件:
這是因為內層按鈕與外層區塊各有一個 onClick,當我們點擊內層區塊的觸發按鈕,會再往外層傳遞,稱為事件冒泡(bubbling)。
這樣有點麻煩,如果外層也有自己的事件要處理,就會一起被觸發,常見的情境像是表格內的按鈕,跟單元格要處理的事件是分開的,但按下內層的按鈕,外層單元格的事件也動作了。
這時可以利用stopPropagation阻止繼續冒泡,在 EventDemo 裡,只把「測試事件傳遞」按鈕的 onClick 換成:
onClick={(event) => {
event.stopPropagation();
console.log("[事件] 內層按鈕收到點擊");
}}
React 會把事件物件(event object)傳給處理函式,這裡取名 event,也常看到寫成 e。
event.stopPropagation() 會阻止事件繼續向上冒泡。存檔後清空 Console,再按一次,就會只剩內層按鈕訊息。
| 寫法 | 用途 |
|---|---|
event.stopPropagation() |
阻止事件繼續傳遞;這裡讓外層的 onClick 不再收到按鈕的點擊 |
event.preventDefault() |
阻止瀏覽器預設行為,例如連結跳頁或表單提交,不會因此阻止冒泡 |
React:Stopping propagation
React:Preventing default behavior
事件傳遞還有捕獲階段,可以透過 onClickCapture 處理,看這裡React:Event propagation
為什麽今天沒有錯誤修正練習呢,因為這只是開胃菜,我一開始以為很簡單,直到後面一發不可收拾,才決定要重頭再學一次。![]()